{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## Открытый курс по машинному обучению. Сессия № 2\n",
    "\n",
    "### <center> Автор материала: Андрей Лукьяненко (@artgor)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# <center> Индивидуальный проект по анализу данных </center>\n",
    "## <center> Маркетинговые кампании португальских банков и их успешность\n",
    "\n",
    "Данные взяты [отсюда](#https://archive.ics.uci.edu/ml/datasets/Bank%20Marketing). Весят больше 5 Мб, так что не выкладываю в репозиторий.\n",
    "\n",
    "Датасет содержит информацию о прямых маркетинговых кампаниях (звонки) португальских банков и о том, согласился ли человек сделать депозит.\n",
    "\n",
    "\n",
    "Классическая задача для прогнозирования в банках - скоринг клиентов для выявления людей с высокой вероятностью дефолта кредита (то есть неплатежа). Долгое время этот анализ осуществлялся с помощью статистических методов, но машинное обучение постепенно начинает играть всё большую роль. Появляются и другие задачи. Так я, работая в банке, занимался предсказанием вероятности отклика клиентов на банковское кросс-сейл предложение.\n",
    "\n",
    "В данном случае стоит задача предсказания вероятности успеха звонка. Основная цель - снизить количество звонков. Известно, что большинство холодных звонков завершаются неудачами. Если есть возможность определить каким людям \"лучше\" звонить, то это снизит расходы на маркетинговые кампании."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Переменные:\n",
    "\n",
    "### Данные о клиентах:\n",
    "\n",
    "1. age: возраст.\n",
    "2. job: типа работы ('admin.','blue-collar','entrepreneur','housemaid','management','retired','self-employed','services','student','technician','unemployed','unknown').\n",
    "3. marital: семейное положение ('divorced','married','single','unknown'; примечание: 'divorced' означает разведённость или вдовство).\n",
    "4. education: образование. ('basic.4y','basic.6y','basic.9y','high.school','illiterate','professional.course','university.degree','unknown').\n",
    "5. default: есть ли кредит в состоянии дефотла ('no','yes','unknown').\n",
    "6. housing: наличие ипотеки ('no','yes','unknown').\n",
    "7. loan: есть ли активные займы ('no','yes','unknown').\n",
    "\n",
    "### Связанное с прошлой маркетинговой компанией:\n",
    "8. contact: как связывались ('cellular','telephone'), видимо стационарный телефон или мобильник.\n",
    "9. month: в какой месяц связывались в последний раз.\n",
    "10. day_of_week: в какой день недели связывались в последний раз.\n",
    "11. duration: сколько времени (в секундах) говорили при прошлом звонке. Эту переменную нельзя использовать в анализе потому, что для новых звонков это значение будет неизвестно. И при окончании звонка будет известен результат = целевая переменная.\n",
    "\n",
    "### Другие параметры:\n",
    "12. campaign: сколько раз с клиентом связывались во время текущей кампании (учитывая последний контакт).\n",
    "13. pdays: количество дней прошедших с последнего контакта прошлой кампании. 999 - не было такого контакта.\n",
    "14. previous: количество контактов с этим клиентом до этой кампании.\n",
    "15. poutcome: результат прошлой кампании. ('failure','nonexistent','success')\n",
    "\n",
    "### Макроэкономика\n",
    "16. emp.var.rate: employment variation rate - квартальный индикатор (numeric).\n",
    "17. cons.price.idx: consumer price index - месячный индикатор (numeric).\n",
    "18. cons.conf.idx: consumer confidence index - месячный индикатор (numeric).\n",
    "19. euribor3m: euribor 3 month rate - дневной индикатор (numeric).\n",
    "20. nr.employed: number of employees - месячный индикатор (numeric).\n",
    "\n",
    "Целевая переменная:\n",
    "21. y - has the client subscribed a term deposit? (binary: 'yes','no')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import seaborn as sns\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "\n",
    "from sklearn.model_selection import train_test_split, cross_val_score, learning_curve, StratifiedKFold, GridSearchCV\n",
    "from sklearn.metrics import roc_auc_score, roc_curve\n",
    "from sklearn import preprocessing\n",
    "from sklearn import linear_model\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "import pandas_profiling\n",
    "\n",
    "pd.set_option(\"display.max_columns\", 30)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data = pd.read_csv('data/bank-additional-full.csv', sep=';')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Общая информация о датасете\n",
    "\n",
    "Есть такая библиотека - pandas_profiling. Её можно использовать для анализа данных - она подобна 'describe', но выдаёт ещё больше статистики по данным, плюс показывает гистограммы распределений."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pandas_profiling.ProfileReport(data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.heatmap(data.corr(), annot=True, fmt = \".2f\", cbar = True, cmap='PuOr')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из этого отчёта можно сделать следующие выводы:\n",
    "\n",
    "* Пропущенных значений нет, зато есть 12 строк с дупликатами;\n",
    "* 11 переменных являются категориальными и 10 вещественными;\n",
    "* 3 переменные - euribor3m, emp.var.rate, nr.employed сильно скоррелированы. Но причина может быть в том, что эти показатели меняются не слишком часто и не слишком сильно. В дальнейшем решим, что делать с этими переменными;\n",
    "* Максимальный возраст - 98. Много, но в пределах возможного;\n",
    "* Большинству людей звонили 1-2 раза (переменная campaign), а людей, которым делали не больше 5 звонков больше 90%. Но есть отдельные случаи, когда звонили больше 40 и даже больше 50 раз. Если использовать эту переменную как вещественную, то выбросы будут слишком сильными, поэтому лучше относиться к ней как к категориальной и преобразовать. Это же релевантно и для переменной previous;\n",
    "* Есть категориальные переменные, у которых слишком мало значений по одной из категорий, значит необходимо объединять некоторые из этих категорий. В числе таких переменных default, education, housing, job, loan, marital, month, poutcome;\n",
    "* У переменной pdays одна из категорий имеет 96% значений, то есть этим клиентам ни разу не звонили. Переменную стоит преобразовать в бинарную или вообще отбросить;\n",
    "* Ну и, конечно, стоит отменить, что у целевой переменной 88,7% значений - \"yes\", а остальные - \"no\"; значит у нас дисбаланс классов;"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Обработка и трансформация признаков."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Categorical\n",
    "Для начала сразу закодируем целевую переменную для удобства."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "le = preprocessing.LabelEncoder()\n",
    "data['y'] = le.fit_transform(data['y'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В банковской сфере есть немало своих способов анализа переменных. Один из них - расчёт показателя Information Value (IV). Он используется для оценки важности категориальных признаков - чем лучше переменная помогает \"дискриминировать\" целевую переменную, тем выше IV. [Здесь](#http://ucanalytics.com/blogs/information-value-and-weight-of-evidencebanking-case/) приводится хороший пример того, как рассчитывается этот показатель. А функция ниже реализовывает его расчёт.\n",
    "\n",
    "При этом использовать его следует аккуратно. Слишком высокие значения (больше 0.5) скорее подозрительны, чем очень хороши."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def calc_iv(data, target, feature):\n",
    "    df = pd.DataFrame(index = data[feature].unique(),\n",
    "                      data={'% responders': data.groupby(feature)[target].sum() / np.sum(data[target])})\n",
    "    df['% non-responders'] = (data.groupby(feature)[target].count() - data.groupby(feature)[target].sum()) / (len(data[target]) - np.sum(data[target]))\n",
    "    df['WOE'] = np.log(df['% responders'] / df['% non-responders'])\n",
    "    df['DG-DB'] = df['% responders'] - df['% non-responders']\n",
    "    df['IV'] = df['WOE'] * df['DG-DB']\n",
    "    return df, np.sum(df['IV'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "calc_iv(data, 'y', 'job')[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(calc_iv(data, 'y', 'job')[1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "А следующая функция будет использоваться для того, чтобы показать взаимосвязь категориальных переменных с целевой. Она показывает количества по категориям (абсолютные и относительные), а также показывает график на котором отображены количества по категориям и среднее значение целевой переменной. Также отображается IV."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def feature_stat(data_, feature, target_name):\n",
    "    data = data_.copy()\n",
    "    print('Counts:')\n",
    "    print(data.groupby(feature)[target_name].count().sort_values(ascending=False))\n",
    "    print('Frequencies:')\n",
    "    print(data[feature].value_counts(normalize=True, dropna=False))\n",
    "    x = [i for i in data.groupby(feature)[target_name].count().index]\n",
    "    \n",
    "    if data[feature].isnull().any():\n",
    "        if str(data[feature].dtype) == 'category':\n",
    "            data[feature].cat.add_categories(['None'], inplace=True)\n",
    "        data[feature].fillna('None', inplace=True)\n",
    "        \n",
    "    y1 = [i for i in data.groupby(feature)[target_name].count().values]\n",
    "    y2 = [i for i in data.groupby(feature)[target_name].mean().values]\n",
    "    ind = np.arange(len(data[feature].unique()))\n",
    "    fig, ax1 = plt.subplots(figsize=(4, 3))\n",
    "    ax1.bar(ind, y1, align='center', width=0.4, alpha=0.7)\n",
    "    ax1.set_xlabel(feature)\n",
    "    ax1.set_ylabel('Counts', color='b')\n",
    "    ax1.tick_params('y1', colors='b')\n",
    "    ax2 = ax1.twinx()\n",
    "    ax2.plot(ind, y2, 'r')\n",
    "    ax2.set_ylabel('Mean rate', color='r')\n",
    "    ax2.tick_params('y2', colors='r')\n",
    "    plt.xticks(ind, x, rotation=45)\n",
    "    ax1.set_xticklabels(x, rotation=35)\n",
    "    plt.grid(False)\n",
    "    plt.show()\n",
    "    _, iv = calc_iv(data, target_name, feature)\n",
    "    print('IV: ', iv)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Кстати говоря, здесь удобно использовать виджеты - я делаю выпадающий список из релевантных переменных и могу смотреть график для нужной, а не прокручивать экран в поисках интересующего меня графика."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from ipywidgets import interact, interactive, interact_manual\n",
    "def a(feature):\n",
    "    feature_stat(data, feature, 'y')\n",
    "    \n",
    "cats = ['housing', 'marital', 'education', 'default', 'job', 'loan',\n",
    "       'contact', 'month', 'day_of_week']\n",
    "interact(a, feature=cats)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Дальше рассмотрим переменные, которые требуют отдельного внимания."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Job"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'job', 'y')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что есть несколько категорий, в которых слишком мало значений. Это приводит к нестабильности. Одним из решений будет объединений таких маленьких категорий. Я объединю все категории, в которых меньше 5% значений."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "z = data['job'].value_counts(normalize=True, dropna=False)\n",
    "to_remove = [e for i, e in enumerate(z.index) if z.values[i] < 0.05]\n",
    "data.loc[data['job'].isin(to_remove) == True, 'job'] = 'Others'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### marital"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'marital', 'y')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Объединим unknown и single, к тому же они скорее всего одинаковы.\n",
    "data.loc[data['marital'] == 'unknown', 'marital'] = 'single'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### education"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'education', 'y')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "У двух категорий совсем мало значений, объединю их с самым низшим уровнем образования."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data.loc[data['education'].isin(['unknown', 'illiterate']) == True, 'education'] = 'basic.4y'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### default"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'default', 'y')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#Отсутствие информации может означать как отсутствие дефолта, так и попытку его сокрытия. Объединю меньшие категории.\n",
    "data.loc[data['default'] == 'yes', 'default'] = 'unknown'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### housing"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'housing', 'y')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Заодно хочу заметить: видно, что IV очень маленький. Это неудивительно: разница в среднем значении целевой переменной по категориям всего лишь 0.008."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data.loc[data['housing'] == 'unknown', 'housing'] = 'no'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### loan"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'loan', 'y')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data.loc[data['loan'] == 'unknown', 'loan'] = 'no'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### contact"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'contact', 'y')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Здесь просто хочу отметить, что эта переменная является весьма хорошей - 2 категории со значительно различающимися значениями целевой переменной."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "### month"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'month', 'y')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Я долго думал, что делать с этой переменной, в итоге решил объединять категории по тому же принципу, что и в job."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "z = data['month'].value_counts(normalize=True, dropna=False)\n",
    "to_remove = [e for i, e in enumerate(z.index) if z.values[i] < 0.05]\n",
    "data.loc[data['month'].isin(to_remove) == True, 'month'] = 'Others'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'month', 'y')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### pdays"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#Как и говорил выше - превращаю переменную в бинарную.\n",
    "data.loc[data['pdays'] != 999, 'pdays'] = 0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'pdays', 'y')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "На переменную надо будет внимательно смотреть - она может быть слишком нестабильной."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### poutcome"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'poutcome', 'y')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "С одной стороны разница между успехом и неуспехом большая не только по смыслу, но и по среднему значению целевой переменной. Если бы это было соревнование, стоило бы всё оставить так, как есть. Но слишком мало клиентов с success, так что объединять категории необходимо. И новая категория будет иметь другой смысл: то, что человеку звонили в рамках прошлой компании."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data.loc[data['poutcome'] != 'nonexistent', 'poutcome'] = 'existent'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'poutcome', 'y')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Campaign\n",
    "\n",
    "Как уже говорилось, для этой переменной необходимо объединить часть категорий. Для удобства я приравняю все значения больше 5 к 5."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "z = data['campaign'].value_counts(normalize=True, dropna=False)\n",
    "to_remove = [e for i, e in enumerate(z.index) if z.values[i] < 0.05]\n",
    "data.loc[data['campaign'].isin(to_remove) == True, 'campaign'] = 5"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'campaign', 'y')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### previous"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data.loc[data['previous'] > 1, 'previous'] = 1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'previous', 'y')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В итоге имеем бинарную переменную."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Создание новых переменных.\n",
    "\n",
    "Есть много разных способов создания новых переменных. Один из них - строить графики и на них искать взаимозависимости переменных."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.pairplot(data=data, hue='y', vars=['age', 'duration', 'campaign',  'emp.var.rate', 'cons.price.idx',\n",
    "                                       'cons.conf.idx', 'euribor3m', 'nr.employed'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Но здесь нужна аккуратность. Может показаться, что переменная duration в сочетании с другими переменными хорошо различает целевую переменную. Но по факту таких значений просто мало, поэтому перекос вполне возможен."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ещё один вариант - попробовать посмотреть как себя ведут пары категориальных переменных."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.crosstab(data.education, data.default)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.crosstab(data.education, data.default, values=data.y, aggfunc=np.mean)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Здесь можно выделить 3 группы людей - с неизвестным состоянием дефолта, без дефолта и с образованием basic.6y или basic.9y, остальные. Попробуем создать такую переменную."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data['education_default'] = 0\n",
    "data.loc[(data.default == 'no') & (data.education.isin(['basic.6y', 'basic.9y'])), 'education_default'] = 1\n",
    "data.loc[(data.default == 'no') & (data.education.isin(['basic.6y', 'basic.9y']) == False), 'education_default'] = 2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_stat(data, 'education_default', 'y')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Вроде получилась хорошая переменная."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#Вот так выглядит датасет сейчас.\n",
    "data.head(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выбор моделей, метрики и предобработка данных.\n",
    "\n",
    "Как уже говорилось, в датасете присутствует дисбаланс классов. Самая распространённая метрика для бинарной классификации с дисбалансом данных - ROC AUC, и именно её я буду использовать.\n",
    "\n",
    "Существует много алгоритмов, которые подходят для классификации несбалансированных данных, в данном случае я решил попробовать случайные леса и логистическую регрессию.\n",
    "\n",
    "* RandomForestClassifier - алгоритм, который хорош для классификации, особенно когда имеется много категориальных и вещественных признаков. Для этой модели достаточно закодировать категориальные переменные, нормализовывать данные не нужно;\n",
    "* LogisticRegressionCV - логистическая регрессия является классической моделью для подобных проблем. А эта вариация позволяет подбирать коэффициент регуляризации на кросс-валидации. Но в данном случае нужно применять one hot encoding для категориальных переменных и нормализовывать вещественные;\n",
    "\n",
    "Соответственно будет 2 набора данных."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#Отбрасываю дубликаты.\n",
    "data = data.drop_duplicates()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for col in ['housing', 'loan', 'contact', 'pdays', 'poutcome', 'default']:\n",
    "    le = preprocessing.LabelEncoder()\n",
    "    data[col] = le.fit_transform(data[col])\n",
    "    \n",
    "for col in ['job', 'marital', 'education', 'month', 'day_of_week', 'campaign', 'education_default']:\n",
    "    dummies = pd.get_dummies(data[col], drop_first=True, prefix='{}_'.format(col))\n",
    "    data.drop(col, axis=1, inplace=True)\n",
    "    data = data.join(dummies)\n",
    "    \n",
    "data1 = data.copy()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_rf = data.drop(['duration', 'y'],axis=1)\n",
    "y_rf = data['y']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Разбиваю данные на тренировочные и валидационные (33%). Для фиксирования результатов задаю random_state, а также stratify, для корректной работы с дисбалансными классами."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_rf, X_valid_rf, y_train_rf, y_valid_rf = train_test_split(X_rf, y_rf, test_size=0.33, random_state=42, stratify=y_rf)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "С одной стороны, нормализацию данных для LogisticRegressionCV можно сделать прямо сейчас, но поскольку мы хотим честно провалидироваться, я вначале разделю данные, а уже потом буду нормализировать их."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_lr = data.drop(['duration', 'y'],axis=1)\n",
    "y_lr = data['y']\n",
    "X_train_lr, X_valid_lr, y_train_lr, y_valid_lr = train_test_split(X_lr, y_lr, test_size=0.33, random_state=42, stratify=y_lr)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scaler = StandardScaler()\n",
    "norm_cols = ['age', 'pdays', 'emp.var.rate', 'cons.price.idx', 'cons.conf.idx', 'euribor3m', 'nr.employed']\n",
    "scaler.fit(X_train_lr[norm_cols])\n",
    "X_train_lr = scaler.transform(X_train_lr[norm_cols])\n",
    "X_valid_lr = scaler.transform(X_valid_lr[norm_cols])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Настройка параметров моделей."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### LogisticRegressionCV\n",
    "\n",
    "Для LogisticRegressionCV не так много параметров для настройки. Набор коэффициентов по умолчанию - это числа от 1e-4 до 1e4 в логарифмической шкале. Также стоит указать метрику roc_auc."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lrcv = linear_model.LogisticRegressionCV(n_jobs=-1, scoring='roc_auc')\n",
    "lrcv.fit(X_train_lr, y_train_lr)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### RandomForestClassifier\n",
    "\n",
    "При настройке гиперпараметров модели необходимо соблюдать баланс между гибкостью и устойчивостью. Отсутствие ограничений по max_features и max_depth обычно повышает гибкость, но стоит пробовать и другие значения. n_estimators - один из основных параметров, который несет похожую смысловую нагрузку. Но большое количество деревьем может требовать слишком больших затрат ресурсов. Критерий определяет принцип разделения для деревьев. Оставшиеся 2 параметра дополнительны, но тоже могут помочь."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "forest = RandomForestClassifier(class_weight='balanced', random_state=42)\n",
    "\n",
    "parameter_grid = {'n_estimators' : [10, 30, 150],\n",
    "                  'criterion' : ['entropy', 'gini'],\n",
    "                  'max_features' : ['sqrt', None],\n",
    "                  'max_depth' : [5, 10, None],\n",
    "                  'min_samples_split' : [2, 15],\n",
    "                  'max_leaf_nodes' : [100, None],\n",
    "                 }\n",
    "\n",
    "grid_search = GridSearchCV(forest, param_grid=parameter_grid, scoring='roc_auc', cv=StratifiedKFold(3), verbose=2, n_jobs=-1)\n",
    "grid_search.fit(X_train_rf, y_train_rf)\n",
    "print('Best score: {}'.format(grid_search.best_score_))\n",
    "print('Best parameters: {}'.format(grid_search.best_params_))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "forest1 = RandomForestClassifier(class_weight='balanced', \n",
    "                                n_estimators=grid_search.best_params_['n_estimators'],\n",
    "                                max_features=grid_search.best_params_['max_features'],\n",
    "                                max_depth=grid_search.best_params_['max_depth'],\n",
    "                                min_samples_split=grid_search.best_params_['min_samples_split'],\n",
    "                                max_leaf_nodes=grid_search.best_params_['max_leaf_nodes'])\n",
    "forest1.fit(X_train_rf, y_train_rf)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Сравнение моделей"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(6, 4))\n",
    "y_pred_train_lr = lrcv.predict_proba(X_train_lr)[:, 1]\n",
    "fpr_train_lr, tpr_train_lr, thresholds_train_lr = roc_curve(y_train_lr, y_pred_train_lr)\n",
    "plt.plot(fpr_train_lr, tpr_train_lr, label='AUC LR train = %0.2f' % roc_auc_score(y_train_lr, y_pred_train_lr))\n",
    "y_pred_val_lr = lrcv.predict_proba(X_valid_lr)[:, 1]\n",
    "fpr_val_lr, tpr_val_lr, thresholds_val_lr = roc_curve(y_valid_lr, y_pred_val_lr)\n",
    "plt.plot(fpr_val_lr, tpr_val_lr, label='AUC LR valid = %0.2f' % roc_auc_score(y_valid_lr, y_pred_val_lr))\n",
    "\n",
    "y_pred_train_rf = grid_search.predict_proba(X_train_rf)[:, 1]\n",
    "fpr_train_rf, tpr_train_rf, thresholds_train_rf = roc_curve(y_train_rf, y_pred_train_rf)\n",
    "plt.plot(fpr_train_rf, tpr_train_rf, label='AUC RF train = %0.2f' % roc_auc_score(y_train_rf, y_pred_train_rf))\n",
    "y_pred_val_rf = grid_search.predict_proba(X_valid_rf)[:, 1]\n",
    "fpr_val_rf, tpr_val_rf, thresholds_val_rf = roc_curve(y_valid_rf, y_pred_val_rf)\n",
    "plt.plot(fpr_val_rf, tpr_val_rf, label='AUC RF valid = %0.2f' % roc_auc_score(y_valid_rf, y_pred_val_rf))\n",
    "\n",
    "plt.plot([0, 1], [0, 1], color='black', linestyle='--', label='0.5')\n",
    "plt.xlabel('False Positive Rate')\n",
    "plt.ylabel('True Positive Rate')\n",
    "plt.title('Receiver operating characteristic example')\n",
    "plt.legend(loc=\"lower right\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scores = cross_val_score(lrcv, X_train_lr, y_train_lr, cv=5, scoring='roc_auc')\n",
    "print('Cross-validation: mean value is {0} with std {1}.'.format(np.round(np.mean(scores), 4),\n",
    "                                                                 np.round(np.std(scores), 4)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scores = cross_val_score(forest1, X_train_rf, y_train_rf, cv=5, scoring='roc_auc')\n",
    "print('Cross-validation: mean value is {0} with std {1}.'.format(np.round(np.mean(scores), 4),\n",
    "                                                                 np.round(np.std(scores), 4)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "RandomForestClassifier оказался значительно лучше. ROC AUC на валидационных данных 0.81, на кросс-валидации 0.79.\n",
    "\n",
    "Давайте посмотрим на важность признаков:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "indices = np.argsort(forest1.feature_importances_)[::-1]\n",
    "\n",
    "print('Feature ranking:')\n",
    "for f in range(X_train_rf.shape[1]):\n",
    "    print('%d. feature %d %s (%f)' % (f + 1, indices[f], X_train_rf.columns[indices[f]],\n",
    "                                      forest1.feature_importances_[indices[f]]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Топ 3 фичи оказалисись высоко скоррелированными фичами. По идее использование коррелирующих признаков не должно сильно влиять на качество модели. Попробуем проверить."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "forest1.fit(X_train_rf.drop(['nr.employed', 'emp.var.rate'], axis=1), y_train_rf)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scores = cross_val_score(forest1, X_train_rf, y_train_rf, cv=5, scoring='roc_auc')\n",
    "print('Cross-validation: mean value is {0} with std {1}.'.format(np.round(np.mean(scores), 4),\n",
    "                                                                 np.round(np.std(scores), 4)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Действительно, значение roc_auc на кросс-валидации практически не изменилось."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Кривые обучения и валидации"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None,\n",
    "                        n_jobs=1, train_sizes=np.linspace(.1, 1.0, 5)):\n",
    "    '''\n",
    "    http://scikit-learn.org/stable/auto_examples/model_selection/plot_learning_curve.html\n",
    "    '''\n",
    "    \n",
    "    plt.figure()\n",
    "    plt.title(title)\n",
    "    if ylim is not None:\n",
    "        plt.ylim(*ylim)\n",
    "    plt.xlabel(\"Training examples\")\n",
    "    plt.ylabel(\"Score\")\n",
    "    train_sizes, train_scores, test_scores = learning_curve(\n",
    "        estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes, scoring='roc_auc')\n",
    "    train_scores_mean = np.mean(train_scores, axis=1)\n",
    "    train_scores_std = np.std(train_scores, axis=1)\n",
    "    test_scores_mean = np.mean(test_scores, axis=1)\n",
    "    test_scores_std = np.std(test_scores, axis=1)\n",
    "    plt.grid()\n",
    "\n",
    "    plt.fill_between(train_sizes, train_scores_mean - train_scores_std,\n",
    "                     train_scores_mean + train_scores_std, alpha=0.1,\n",
    "                     color=\"r\")\n",
    "    plt.fill_between(train_sizes, test_scores_mean - test_scores_std,\n",
    "                     test_scores_mean + test_scores_std, alpha=0.1, color=\"g\")\n",
    "    plt.plot(train_sizes, train_scores_mean, 'o-', color=\"r\",\n",
    "             label=\"Training score\")\n",
    "    plt.plot(train_sizes, test_scores_mean, 'o-', color=\"g\",\n",
    "             label=\"Cross-validation score\")\n",
    "\n",
    "    plt.legend(loc=\"best\")\n",
    "    return plt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(6, 4))\n",
    "plot_learning_curve(forest1, 'Random Forest', X_train_rf, y_train_rf, cv=3, n_jobs=4);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "График в целом адекватный, но видно, что данных было недостаточно. Сбор дополнительных данных может привести к улучшению качества прогноза."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Построю кривые валидации для двух параметров."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#http://scikit-learn.org/stable/auto_examples/model_selection/plot_validation_curve.html\n",
    "plt.figure(figsize=(6, 4))\n",
    "param_range=np.array([10, 30, 50, 150])\n",
    "train_scores, test_scores = validation_curve(forest1, X_train_rf, y_train_rf, param_name=\"n_estimators\",\n",
    "                                             param_range=param_range, cv=5, scoring=\"roc_auc\", n_jobs=-1)\n",
    "train_scores_mean = np.mean(train_scores, axis=1)\n",
    "train_scores_std = np.std(train_scores, axis=1)\n",
    "test_scores_mean = np.mean(test_scores, axis=1)\n",
    "test_scores_std = np.std(test_scores, axis=1)\n",
    "\n",
    "plt.title(\"Validation Curve\")\n",
    "plt.xlabel(\"n_estimators\")\n",
    "plt.ylabel(\"Score\")\n",
    "#plt.ylim(0.0, 1.1)\n",
    "lw = 2\n",
    "plt.plot(param_range, train_scores_mean, label=\"Training score\",\n",
    "             color=\"darkorange\", lw=lw)\n",
    "plt.fill_between(param_range, train_scores_mean - train_scores_std,\n",
    "                 train_scores_mean + train_scores_std, alpha=0.2,\n",
    "                 color=\"darkorange\", lw=lw)\n",
    "plt.plot(param_range, test_scores_mean, label=\"Cross-validation score\",\n",
    "             color=\"navy\", lw=lw)\n",
    "plt.fill_between(param_range, test_scores_mean - test_scores_std,\n",
    "                 test_scores_mean + test_scores_std, alpha=0.2,\n",
    "                 color=\"navy\", lw=lw)\n",
    "plt.legend(loc=\"best\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "При увеличении значения n_estimators качество модели почти не изменяется."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(6, 4))\n",
    "param_range=np.array([5, 10, 41])\n",
    "train_scores, test_scores = validation_curve(forest1, X_train_rf, y_train_rf, param_name=\"max_depth\",\n",
    "                                             param_range=param_range, cv=5, scoring=\"roc_auc\", n_jobs=-1)\n",
    "train_scores_mean = np.mean(train_scores, axis=1)\n",
    "train_scores_std = np.std(train_scores, axis=1)\n",
    "test_scores_mean = np.mean(test_scores, axis=1)\n",
    "test_scores_std = np.std(test_scores, axis=1)\n",
    "\n",
    "plt.title(\"Validation Curve\")\n",
    "plt.xlabel(\"max_depth\")\n",
    "plt.ylabel(\"Score\")\n",
    "#plt.ylim(0.0, 1.1)\n",
    "lw = 2\n",
    "plt.plot(param_range, train_scores_mean, label=\"Training score\",\n",
    "             color=\"darkorange\", lw=lw)\n",
    "plt.fill_between(param_range, train_scores_mean - train_scores_std,\n",
    "                 train_scores_mean + train_scores_std, alpha=0.2,\n",
    "                 color=\"darkorange\", lw=lw)\n",
    "plt.plot(param_range, test_scores_mean, label=\"Cross-validation score\",\n",
    "             color=\"navy\", lw=lw)\n",
    "plt.fill_between(param_range, test_scores_mean - test_scores_std,\n",
    "                 test_scores_mean + test_scores_std, alpha=0.2,\n",
    "                 color=\"navy\", lw=lw)\n",
    "plt.legend(loc=\"best\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Заметно, что глубина 10 - примерно оптимум. Меньшая глубина недостаточна для моделирования данных, большая приводит к переобучению."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выводы\n",
    "\n",
    "Было выяснено, что RandomForestClassifier хорошо работает для имеющихся у нас данных. Получение высокого значения roc_auc значит, что можно выбрать такой порог принятия решений, что можно отобрать людей, которые с высокой вероятностью дадут положительный отклик на маркетинговую кампанию.\n",
    "\n",
    "Естественно, есть пути улучшения результатов:\n",
    "* более точная настройка параметров модели;\n",
    "* использование других моделей;\n",
    "* сбор большего объёма данных;\n",
    "* аккуратный feature engineering;"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
